import numpy as np


# 定义sigmoid激活函数
def sigmoid(x):
    """
    Sigmoid激活函数定义
    """
    return 1.0 / (1.0 + np.exp(-x))


# 定义sigmoid激活函数的导数
def d_sigmoid(x):
    """
    Sigmoid函数的导数
    """
    return sigmoid(x) * (1 - sigmoid(x))


# 定义神经网络类
class FCN(object):
    """
    全连接神经网络纯手工实现
    """

    def __init__(self, size):
        """
        初始化函数，使用size的容生成全连接神经网络，初始化阈值参数向量b和权重向量w，数值符合标准正态分布
        :param size:是一个列表，包含了全连接神经网络的每一层的神经元个数，列表的长度就是神经网络的层数
        """
        # 神经网络各层神经元个数
        self.size = size
        # 神经网络层数 layers
        self._layers = len(size)
        # 隐藏层和输出层的输出阈值参数向量b,以[728, 12, 10]为例子,将生成两个阈值参数向量b,分别属于隐藏层和输出层.大小分别为12*1,10*1
        self._output_b = [np.random.randn(y, 1) for y in size[1:]]
        # 隐藏层和输出层的输入权重向量w,以[728, 12, 10]为例子,将生成两个权重向量w,分别属于隐藏层和输出层,大小分别为728*12,12*10
        self._input_w = [np.random.randn(y, x) for x, y in zip(size[:-1], size[1:])]
        # 神经网络损失函数列表，用于存储每轮计算的损失函数值
        self.list_loss = []
        # print(self._output_b[0].shape)
        # print(self._output_b[1].shape)
        # print(self._input_w[0].shape)
        # print(self._input_w[1].shape)

    def forward(self, a):
        """
        向前经计算，返回神经网络的的输出。公式为：
        output = sigmoid(w * x + b)
        以[784,12,10]为例，权重向量大小分别为[12x784, 10x12]，偏置向量大小分别为[12x1, 10x1]
        输入向量为 784x1.
        矩阵的计算过程为：
            12x784 * 784x1 = 12x1
            12x1 + 12x1 = 30x1

            10x12 * 12x1 = 10x1
            10x1 + 10x1 = 10x1
            故最后的输出是10x1的向量，即代表了10个数字。
        :param a: 神经网络的输入，以[782， 12， 10]神经网络为例，输入为782*1的向量
        :return: 神经网络的计算输出
        """
        for b, w in zip(self._output_b, self._input_w):
            a = sigmoid(np.dot(w, a) + b)
        return a

    def SGD(self, train_data, epochs, batch_size, alpha, test_data=None):
        """
        使用小批量随机梯度下降训练神经网络
        :param test_data:
        :param train_data:以(array(x), array(y))的元组形式的列表，代表训练数据的输入和输出
        :param epochs:训练轮次
        :param batch_size:小批量样本的数据集大小即每轮用于训练的图片数
        :param alpha:学习率
        :return:
        """
        n = len(train_data)
        # 按照轮数进行训练
        for j in range(epochs):
            # 在每轮训练开始之前，将训练集随机打乱，让后均分成batch_size大小的小batch进行学习
            np.random.shuffle(train_data)
            batches = [train_data[k:k + batch_size] for k in range(0, n, batch_size)]
            # 在每轮训练中，将所有的batch样本进行学习
            for batch in batches:
                self.update_batch(batch, alpha)

            if test_data:
                # 计算损失函数
                loss = self.loss(test_data)
                # 将损失函数值添加进损失函数list中
                self.list_loss.append(loss)
                print("Epoch %5d: Loss--%.5f Accuracy--%.2f%%"
                      % (j, loss, self.evaluate(test_data)))

    def update_batch(self, batch, alpha):
        """
        通过小批量随机梯度下降以及反向传播来更新神经网络的权重和偏置向量
        :param batch:当前轮次的小批量数据集
        :param alpha:学习率
        :return:
        """
        temp_b = [np.zeros(b.shape) for b in self._output_b]
        temp_w = [np.zeros(w.shape) for w in self._input_w]

        for x, y in batch:
            # 反向传播，利用链式法则求得对b和w的偏导
            delta_b, delta_w = self.backprop(x, y)
            # 对小批量训练数据集中的每一个求得的偏导数进行累加。增加梯度，使得学习速率更快
            temp_b = [nb + dnb for nb, dnb in zip(temp_b, delta_b)]
            temp_w = [nw + dnw for nw, dnw in zip(temp_w, delta_w)]
        # 使用梯度下降法更新权重w和阈值向量b
        self._output_b = [b - (alpha / len(batch)) * nb
                          for b, nb in zip(self._output_b, temp_b)]
        self._input_w = [w - (alpha / len(batch)) * nw
                         for w, nw in zip(self._input_w, temp_w)]

    def backprop(self, x, y):
        """
        反向传播算法，计算损失对w和b的梯度
        :param x: 训练数据x
        :param y: 训练数据x对应的标签
        :return:
        """
        temp_b = [np.zeros(b.shape) for b in self._output_b]
        temp_w = [np.zeros(w.shape) for w in self._input_w]
        # 向前传播，计算网络的输出
        activation = x
        # 一层一层存储全部的激活值列表
        list_activation = [x]
        # 一层一层存储全部的z向量，即带权输入
        list_z = []
        # 从第二层到输出层，逐层计算神经元的输入值z和输出值activation
        for b, w in zip(self._output_b, self._input_w):
            # 计算网络该层神经元的输入，即z = w*x + b，第一层的神经元输入是数据集数据pixels，将计算结果存入list_
            z = np.dot(w, activation) + b
            list_z.append(z)
            # 计算网络该层神经元的输出，即activation = sigmoid(z)，并将结果存入list_
            activation = sigmoid(z)
            list_activation.append(activation)

        # 反向传播过程
        # 首先计算输出层的误差delta_L
        delta = self.d_loss(activation[-1], y) * d_sigmoid(list_z[-1])
        # 反向存储 损失函数C对b的偏导数
        temp_b[-1] = delta
        # 反向存储 损失函数对w的偏导数
        temp_w[-1] = np.dot(delta, list_activation[-2].transpose())

        # 从输出层到输入层的第二层开始，一次计算每一层的神经元偏导数
        for i in range(2, self._layers):
            # 更新得到前一层的误差delta
            delta = np.dot(self._input_w[-i + 1].transpose(), delta) * d_sigmoid(list_z[-i])
            # 计算损失函数C对b的偏导数delta
            temp_b[-i] = delta
            # 计算损失函数C对w的偏导数
            temp_w[-i] = np.dot(delta, list_activation[-i - 1].transpose())
        # 返回每一层神经元的对b和w的偏导数
        return temp_b, temp_w

    def loss(self, test_data):
        """
        先向前传播预测神经网络的输出值向量，这里为10x1的向量
        计算损失函数 C = 1/2 * 1/len(y(x)) * ∑(∑((y(x)i - yi)^2)))
        test_data形式为[array_x([[],[]```[]],```, [[],[]```[]]), array_y([[],[]```[]],```,[[],[]```[]])]
        :param test_data:测试数据集
        :return:损失函数值
        """
        n = len(test_data)
        forward_results = [(self.forward(x), y) for (x, y) in test_data]
        return 1 / (2 * n) * np.sum(np.sum((x - y) ** 2 for (x, y) in forward_results))

    def d_loss(self, output_activations, label):
        """
        计算损失函数对a的偏导数，损失函数为 C = 1/2 * ||y(x) - a||^2
        求导的结果为：
        C' = y(x) - a
        形参的形式均为 n x (10x1)的向量
        :param output_activations: 预测的输出
        :param label: 实际的label
        :return: C' = y(x) - a
        """
        return output_activations - label

    def evaluate(self, test_data):
        """
        向前传播计算正确的比例
        :param test_data:测试集形式为[[x[],[]],[y[],[]]]
        :return:float型准确率值
        """
        # 向前传播 计算当前轮次下的输出结果，并转化为为与数据集相同的格式
        # np.argmax()函数返回返回的是x中元素最大值所对应的索引值，即预测结果
        results = [(np.argmax(self.forward(x)), np.argmax(y)) for (x, y) in test_data]
        # 计算测试集中预测正确的个数
        accurate_sum = sum(int(x == y) for (x, y) in results)
        # 返回预测正确的比率
        return accurate_sum / len(test_data) * 100



